﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows;
using Microsoft.LiveFX.ResourceModel;

namespace Wintellect.LiveFx.Cloudboard
{
  internal class HotkeyListener : System.Windows.Interop.HwndSource
  {
    private class HotkeyConfigurationScope : IDisposable
    {
      HotkeyListener parent;
      public HotkeyConfigurationScope(HotkeyListener parent)
      {
        this.parent = parent;
        this.parent.DisableHotkeys();
      }
      public void Dispose()
      {
        this.parent.EnableHotkeys();
      }
    }

    private Configuration configuration;
    private Options optionsDialog;
    private AboutBox aboutBox;
    private UpdateChecker updateChecker;
    private System.Windows.Forms.NotifyIcon icon;
    private System.Windows.Forms.MenuItem updateMenuItem;
    private VersionRecord newVersion;
    private Object ringDialogSynchronizationObject = new Object();
    private ClipboardRing visibleRing;
    private bool online;

    public HotkeyListener() : this(Configuration.Instance)
    {
    }

    public HotkeyListener(Configuration configuration) : base(new System.Windows.Interop.HwndSourceParameters())
    {
      if (this.Handle == IntPtr.Zero)
      {
        System.Diagnostics.Debug.Assert(false, "No window handle created for the message pump.");
        return;
      }

      if (configuration == null)
      {
        throw new ArgumentNullException("configuration");
      }

      this.configuration = configuration;

      this.CreateNotificationIcon();

      this.GoOnline();

      if (this.configuration.CheckForUpdates)
      {
        this.updateChecker = new UpdateChecker();
        this.updateChecker.NewVersionAvailable += new EventHandler<NewVersionEventArgs>(updateChecker_NewVersionAvailable);
        this.updateChecker.Start();
      }

      System.Windows.Interop.HwndSource.FromHwnd(this.Handle).AddHook(this.WndProc);

      this.EnableHotkeys();
    }

    private void EnableHotkeys()
    {
      KeyModifier copyHotkeyModifiers =
        (this.configuration.CopyHotkey.Alt ? KeyModifier.Alt : KeyModifier.None) |
        (this.configuration.CopyHotkey.Control ? KeyModifier.Control : KeyModifier.None) |
        (this.configuration.CopyHotkey.Shift ? KeyModifier.Shift : KeyModifier.None);
      if (!NativeMethods.RegisterHotKey(
        this.Handle,
        1,
        copyHotkeyModifiers,
        this.configuration.CopyHotkey.Key.ToKeyCode()))
      {
        EtwTrace.Trace("Unable to register hotkey listener.");
      }

      KeyModifier uiHotkeyModifiers =
        (this.configuration.InsertHotkey.Alt ? KeyModifier.Alt : KeyModifier.None) |
        (this.configuration.InsertHotkey.Control ? KeyModifier.Control : KeyModifier.None) |
        (this.configuration.InsertHotkey.Shift ? KeyModifier.Shift : KeyModifier.None);
      if (!NativeMethods.RegisterHotKey(
        this.Handle,
        2,
        uiHotkeyModifiers,
        this.configuration.InsertHotkey.Key.ToKeyCode()))
      {
        EtwTrace.Trace("Unable to register hotkey listener.");
      }
    }

    private void DisableHotkeys()
    {
      NativeMethods.UnregisterHotKey(this.Handle, 1);
      NativeMethods.UnregisterHotKey(this.Handle, 2);
    }

    void updateChecker_NewVersionAvailable(object sender, NewVersionEventArgs e)
    {
      if (this.updateMenuItem != null)
      {
        this.updateMenuItem.Enabled = true;
      }
      this.newVersion = e.VersionRecord;
      this.icon.ShowBalloonTip(
        10, 
        "New Version", 
        String.Format("Version {0} of Cloudboard is now available!", e.VersionRecord.Version), 
        System.Windows.Forms.ToolTipIcon.Info);
    }

    public bool OnLine
    {
      get
      {
        return this.online;
      }
    }

    private void GoOffline(Exception e)
    {
      if (e != null)
      {
        this.HandleException(e);
      }
      else
      {
        this.icon.ShowBalloonTip(
          10,
          "Unable to connect",
          "Unable to connect to the Live Framework client. Please verify the Live Framework client is running. You may try to reconnect using the 'Go Online' menu item.",
          System.Windows.Forms.ToolTipIcon.Error);
      }

      this.online = false;
      this.configuration.ClipboardsUpdated -= new EventHandler(configuration_ClipboardsUpdated);
      this.SetOfflineMenu();
    }

    private void GoOnline()
    {
      this.online = false;

      try
      {
        configuration.Load();
      }
      catch (Exception e)
      {
        if (!this.HandleException(e))
        {
          throw;
        }
        this.SetOfflineMenu();
        return;
      }

      // If we either don't have a configured clipboard or we find a match
      // for it in the list, prompt the user with the Clipboard management dialog
      if (null == this.configuration.CurrentClipboard)
      {
        using (new HotkeyConfigurationScope(this))
        {
          Options dialog = new Options(this.configuration, OptionsTab.ManageCloudboards);
          dialog.ShowDialog();
        }
      }

      if (null != this.configuration.CurrentClipboard)
      {
        try
        {
          this.configuration.CurrentClipboard.GoOnline();

          configuration.Load();
          this.online = true;

          // Display the cloudboard name. Qualify it with the provisioned users name to disambiguate
          String iconText = String.Format("Connected to '{0}' ({1})",
            this.configuration.CurrentClipboard.Name,
            this.configuration.CurrentClipboard.LiveID);

          // The icon text must be less than 64 characters.
          // If our text is longer than this unfrtunate limit, truncate it and add an ellipse
          if (iconText.Length > 64)
          {
            iconText = iconText.Substring(0, 61);
            iconText += "...";
          }
          this.icon.Text = iconText;

          this.RefreshNotificationIconMenu();

          this.configuration.ClipboardsUpdated += new EventHandler(configuration_ClipboardsUpdated);
        }
        catch (Exception e)
        {
          if (!this.HandleException(e))
          {
            throw;
          }
        }
      }
      else
      {
        this.SetOfflineMenu();
      }
    }

    void configuration_ClipboardsUpdated(object sender, EventArgs e)
    {
      this.RefreshNotificationIconMenu();
    }

    private bool HandleException(Exception e)
    {
      bool output = false;
      if (e is LiveFxException<UnableToConnectToLoe>)
      {
        EtwTrace.Trace(
          "Unable to retrieve clipboard objects - the LOE does not appear to be running:\n{0}",
          e);
        this.icon.ShowBalloonTip(
          10,
          "Unable to connect",
          "Unable to connect to the Live Framework client. Please verify the Live Framework client is running. You may try to reconnect using the 'Go Online' menu item.",
          System.Windows.Forms.ToolTipIcon.Error);
        output = true;
      }
      else if (e is LiveFxException<UnableToAccessLoeData>)
      {
        EtwTrace.Trace(
          "Unable to retrieve clipboard objects - a user does not appear to be signed into the LOE:\n{0}",
          e);
        this.icon.ShowBalloonTip(
          10,
          "Unable to access data", "Unable to download mesh data - please verify that you are signed into the Live Framework client.  You may try to reconnect using the 'Go Online' menu item.",
          System.Windows.Forms.ToolTipIcon.Error);
        output = true;
      }
      return output;
    }

    private void CreateNotificationIcon()
    {
      this.icon = new System.Windows.Forms.NotifyIcon();
      this.icon.Icon = Properties.Resources.Clipboard;

      this.icon.MouseClick += (o, e) =>
      {
        if (e.Button == System.Windows.Forms.MouseButtons.Left)
        {
          this.ShowClipboardRing(IntPtr.Zero, IntPtr.Zero);
        }
      };
      this.icon.Visible = true;
    }

    private void SetOfflineMenu()
    {
      this.icon.ContextMenu = new System.Windows.Forms.ContextMenu();
      System.Windows.Forms.MenuItem goOnline = new System.Windows.Forms.MenuItem("Go Online");
      goOnline.Click += delegate
      {
        this.GoOnline();
      };
      this.icon.ContextMenu.MenuItems.Add(goOnline);

      this.AddMenuFooter();
    }

    private void RefreshNotificationIconMenu()
    {
      this.icon.ContextMenu = new System.Windows.Forms.ContextMenu();

      System.Windows.Forms.MenuItem cloudBoardList = new System.Windows.Forms.MenuItem("Cloudboards");
      foreach (String cloudBoardTitle in from c in this.configuration.Clipboards select c.Title)
      {
        System.Windows.Forms.MenuItem item = cloudBoardList.MenuItems.Add(cloudBoardTitle);
        item.Checked = cloudBoardTitle == this.configuration.CurrentClipboard.Name;
        item.Click += (o, e) =>
        {
          try
          {
            EtwTrace.Trace("Cloudboard selection changed to '{0}'", item.Text);
            Clipboard selected = new Clipboard(item.Text);
            this.configuration.CurrentClipboard = selected;
          }
          catch (System.Net.WebException we)
          {
            EtwTrace.Trace(EtwTraceLevel.Error, EtwTraceFlag.Diagnostic, "Unable to set selected clipboard: exception encountered: {0}", we);
          }
          catch (Exception ex)
          {
            EtwTrace.Trace(EtwTraceLevel.Error, EtwTraceFlag.Diagnostic, "Unexpected exception encountered: {0}", ex);
          }
        };
      }
      this.icon.ContextMenu.MenuItems.Add(cloudBoardList);

      if (this.configuration.CheckForUpdates)
      {
        this.updateMenuItem = new System.Windows.Forms.MenuItem("Download new version");
        this.updateMenuItem.Enabled = false;
        this.updateMenuItem.Click += new EventHandler(this.updateMenuItem_Clicked);
        this.icon.ContextMenu.MenuItems.Add(this.updateMenuItem);
      }

      this.AddMenuFooter();

      // TODO: Need a better wireup here...
      this.configuration.CurrentClipboard.ItemsChanged += (o, e) =>
      {
        this.icon.BalloonTipTitle = String.Format("Cloudboard '{0}' changed.", this.configuration.CurrentClipboard.Name);
        this.icon.BalloonTipIcon = System.Windows.Forms.ToolTipIcon.Info;
        
        if (e.Added.Count > 0)
        {
          this.icon.BalloonTipText = e.Added.First().Resource.Authors.First().Name + " added data.";
        } 
        else if (e.Removed.Count > 0)
        {
          this.icon.BalloonTipText = e.Removed.First().Resource.Authors.First().Name + " removed data.";
        }

        this.icon.ShowBalloonTip(5000);
      };
    }

    private void AddMenuFooter()
    {
      System.Windows.Forms.MenuItem options = new System.Windows.Forms.MenuItem("Options");
      options.Click += new EventHandler(this.options_Clicked);
      this.icon.ContextMenu.MenuItems.Add(options);

      System.Windows.Forms.MenuItem about = new System.Windows.Forms.MenuItem("About " + Constants.AppName);
      about.Click += (o, e) =>
      {
        if (this.aboutBox != null) return;
        this.aboutBox = new AboutBox();
        this.aboutBox.ShowDialog();
        this.aboutBox = null;
      };
      this.icon.ContextMenu.MenuItems.Add(about); 
      
      this.icon.ContextMenu.MenuItems.Add(new System.Windows.Forms.MenuItem("-"));
      
      System.Windows.Forms.MenuItem exit = new System.Windows.Forms.MenuItem("Exit");
      exit.Click += (o, exitEventArgs) => 
      {
        if (System.Windows.Application.Current != null)
        {
          this.icon.Dispose();
          System.Windows.Application.Current.Shutdown();
        }
      };
      this.icon.ContextMenu.MenuItems.Add(exit);
    }

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
       //System.Diagnostics.Debug.Assert(hwnd == WindowHandle); // Only expecting messages for our cached HWND.

       // NOTE: we may process a message multiple times
       // and we have no good way to handle that...
       var result = IntPtr.Zero;

       if (msg == (int)WindowMessage.WM_HOTKEY)
       {
         KeyCode keyCode = (KeyCode)((int)lParam >> 16);
         EtwTrace.Trace("Caught hotkey!");

         // If we're not online, or the options dialog is being displayed, intercept the hotkey
         if (!this.online || this.optionsDialog != null)
         {
           return result;
         }

         IntPtr foregroundWindow = IntPtr.Zero;
         IntPtr foregroundFocus = IntPtr.Zero;
         try
         {
           foregroundWindow = NativeMethods.GetForegroundWindow();
           foregroundFocus = NativeMethods.GetForegroundFocus();
         }
         catch (System.ComponentModel.Win32Exception win32e)
         {
           EtwTrace.Trace(
             EtwTraceLevel.Error, 
             EtwTraceFlag.Diagnostic, 
             "Exception thrown during hotkey handler: {0}", win32e.ToString());
         }

         if (foregroundWindow == IntPtr.Zero || foregroundFocus == IntPtr.Zero)
         {
           return result;
         }

         if (keyCode == this.configuration.InsertHotkey.Key.ToKeyCode())
         {
           this.ShowClipboardRing(foregroundWindow, foregroundFocus);
         }
         else if (keyCode == this.configuration.CopyHotkey.Key.ToKeyCode())
         {
           // Push the selected items(s) (if any) into the clipboard
           NativeMethods.SendWindowCopy(foregroundFocus);

           this.configuration.CurrentClipboard.AddItemFromSystemClipboard();
         }

         handled = true;
       }
 
      return result;
    }

    private void ShowClipboardRing(IntPtr foregroundWindow, IntPtr foregroundFocus)
    {
      EtwTrace.Trace("Foreground Window Handle: {0}\nForeground Focus Handle: {1}", foregroundWindow, foregroundFocus);

      // Guard against more than one clipboard ring being visible at a time
      lock (this.ringDialogSynchronizationObject)
      {
        if (this.visibleRing == null)
        {
          this.visibleRing = new ClipboardRing(this.configuration.CurrentClipboard);
          this.visibleRing.Closed += (o, e) => 
          {
            lock (this.ringDialogSynchronizationObject)
            {
              this.visibleRing = null;
            }
          };
          this.visibleRing.Dispatcher.Invoke(new Action(() =>
          {
            this.visibleRing.Show(foregroundWindow, foregroundFocus);
          }));
        }
      }
    }

    private void options_Clicked(object sender, EventArgs e)
    {
      if (this.optionsDialog != null) return;

      using (new HotkeyConfigurationScope(this))
      {
        this.optionsDialog = new Options(this.configuration);
        lock (this.ringDialogSynchronizationObject)
        {
          this.optionsDialog.Owner = this.visibleRing;
          this.optionsDialog.ShowDialog();
          this.optionsDialog = null;
        }
      }
    }

    private void updateMenuItem_Clicked(object sender, EventArgs e)
    {
      System.Diagnostics.Process.Start(this.newVersion.Uri.ToString());
    }
  }
}
